﻿using System;
using System.Collections.Generic;
using System.Drawing;
using RaytracerLib.Layers;
using RaytracerLib.MathLib;
using TomanuExtensions;
using RaytracerLib.Layers.OverlayCorrectors;
using System.Linq;
using RaytracerLib.Postprocessing;
using RaytracerLib.Samplers;
using System.Diagnostics;
using RaytracerLib.Resamplers.Filters;

namespace RaytracerLib.Resamplers
{
    public class ResizerResampler : FilterResampler
    {
        private ColorArrayFloat m_rays;
        private List<List<PixelWeight>> m_horz_weights;
        private List<List<PixelWeight>> m_vert_weights;
        private int m_scale;

        internal override void SetRayColor(Vector2 a_pixel, ColorFloat a_color)
        {
            a_pixel *= m_scale;
            m_rays.SetColor((int)a_pixel.X, (int)a_pixel.Y, a_color);
        }

        protected override ColorFloat ResamplePixel(int a_x, int a_y)
        {
            var horz_weights = m_horz_weights[a_x];
            var vert_weights = m_vert_weights[a_y];

            ColorFloat color = ColorFloat.Black;

            for (int y = 0; y < vert_weights.Count; y++)
            {
                ColorFloat vert_color = ColorFloat.Black;

                for (int x = 0; x < horz_weights.Count; x++)
                {
                    vert_color += Gamma.LinearToSRGB(m_rays.GetColor(horz_weights[x].Pos, vert_weights[y].Pos)) *
                        horz_weights[x].Weight;
                }

                color += vert_color * vert_weights[y].Weight;
            }

            return Gamma.SRGBToLinear(color);
        }

        internal override void RenderStart(RenderStartPhase a_phase)
        {
            if (a_phase == RenderStartPhase.PrepareObjectToRender)
            {
                UniformSampler sampler = Film.Sampler as UniformSampler;

                if (sampler == null)
                    throw new InvalidOperationException();

                m_scale = sampler.Subresolution;

                m_rays = new ColorArrayFloat(
                    Film.Width * m_scale,
                    Film.Height * m_scale);

                PrecalculateWeights(
                    Film.Width * m_scale,
                    Film.Height * m_scale,
                    Film.Width, Film.Height, 
                    out m_horz_weights, out m_vert_weights, Filter);
            }
        }

        public static void PrecalculateWeights(int a_src_width, int a_src_height,
            int a_dest_width, int a_dest_height, out List<List<PixelWeight>> a_horz_weights,
            out List<List<PixelWeight>> a_vert_weights, Filter a_filter, bool a_optimize = true)
        {
            double scalex = 1.0 * a_dest_width / a_src_width;
            double scaley = 1.0 * a_dest_height / a_src_height;

            double src_ray_x = a_filter.Ray / scalex;
            double src_ray_y = a_filter.Ray / scaley;

            a_horz_weights = new List<List<PixelWeight>>(a_dest_width);

            if (scalex < 1)
            {
                OverlayCorrector overlay_corrector = OverlayCorrector.Create(OverlayMethod.Mirror,
                    a_src_width, a_src_height, (int)(src_ray_x + 2));

                for (int x = 0; x < a_dest_width; x++)
                {
                    double src_center_x = x / scalex;

                    int start_xx = (int)Math.Floor(src_center_x - src_ray_x);
                    int end_xx = (int)Math.Ceiling(src_center_x + src_ray_x);

                    if (a_filter.FilterType == FilterType.NearestNeighbour)
                        src_center_x = src_center_x.Round();

                    List<PixelWeight> weights = new List<PixelWeight>();

                    for (int xx = start_xx; xx <= end_xx; xx++)
                    {
                        weights.Add(new PixelWeight()
                        {
                            Pos = overlay_corrector.CorrectX(xx),
                            Weight = a_filter.Evaluate((src_center_x - xx) * scalex) * scalex
                        });
                    }

                    Debug.Assert(a_filter.Evaluate((src_center_x - (start_xx - 1)) * scalex).IsAlmostEquals(0));
                    Debug.Assert(a_filter.Evaluate((src_center_x - (end_xx + 1)) * scalex).IsAlmostEquals(0));

                    a_horz_weights.Add(weights);
                }
            }
            else
            {
                OverlayCorrector overlay_corrector = OverlayCorrector.Create(OverlayMethod.Mirror,
                    a_src_width, a_src_height, (int)(a_filter.Ray + 2));

                for (int x = 0; x < a_dest_width; x++)
                {
                    double src_center_x = x / scalex;

                    int start_xx = (int)Math.Floor(src_center_x - a_filter.Ray);
                    int end_xx = (int)Math.Ceiling(src_center_x + a_filter.Ray);

                    List<PixelWeight> weights = new List<PixelWeight>();

                    if (a_filter.FilterType == FilterType.NearestNeighbour)
                        src_center_x = src_center_x.Round();

                    for (int xx = start_xx; xx <= end_xx; xx++)
                    {
                        weights.Add(new PixelWeight()
                        {
                            Pos = overlay_corrector.CorrectX(xx),
                            Weight = a_filter.Evaluate(src_center_x - xx)
                        });
                    }

                    Debug.Assert(a_filter.Evaluate(src_center_x - (start_xx - 1)).IsAlmostEquals(0));
                    Debug.Assert(a_filter.Evaluate(src_center_x - (end_xx + 1)).IsAlmostEquals(0));

                    a_horz_weights.Add(weights);
                }
            }

            a_vert_weights = new List<List<PixelWeight>>(a_dest_height);

            if (scaley < 1)
            {
                OverlayCorrector overlay_corrector = OverlayCorrector.Create(OverlayMethod.Mirror,
                    a_src_width, a_src_height, (int)(src_ray_y + 2));

                for (int y = 0; y < a_dest_height; y++)
                {
                    double src_center_y = y / scaley;

                    int start_yy = (int)Math.Floor(src_center_y - src_ray_y);
                    int end_yy = (int)Math.Ceiling(src_center_y + src_ray_y);

                    List<PixelWeight> weights = new List<PixelWeight>();

                    if (a_filter.FilterType == FilterType.NearestNeighbour)
                        src_center_y = src_center_y.Round();

                    for (int yy = start_yy; yy <= end_yy; yy++)
                    {
                        weights.Add(new PixelWeight()
                        {
                            Pos = overlay_corrector.CorrectY(yy),
                            Weight = a_filter.Evaluate((src_center_y - yy) * scaley) * scaley
                        });
                    }

                    Debug.Assert(a_filter.Evaluate((src_center_y - (start_yy - 1)) * scaley).IsAlmostEquals(0));
                    Debug.Assert(a_filter.Evaluate((src_center_y - (end_yy + 1)) * scaley).IsAlmostEquals(0));

                    a_vert_weights.Add(weights);
                }
            }
            else
            {
                OverlayCorrector overlay_corrector = OverlayCorrector.Create(OverlayMethod.Mirror,
                    a_src_width, a_src_height, (int)(a_filter.Ray + 2));

                for (int y = 0; y < a_dest_height; y++)
                {
                    double src_center_y = y / scaley;

                    int start_yy = (int)Math.Floor(src_center_y - a_filter.Ray);
                    int end_yy = (int)Math.Ceiling(src_center_y + a_filter.Ray);

                    List<PixelWeight> weights = new List<PixelWeight>();

                    if (a_filter.FilterType == FilterType.NearestNeighbour)
                        src_center_y = src_center_y.Round();

                    for (int yy = start_yy; yy <= end_yy; yy++)
                    {
                        weights.Add(new PixelWeight()
                        {
                            Pos = overlay_corrector.CorrectY(yy),
                            Weight = a_filter.Evaluate(src_center_y - yy)
                        });
                    }

                    Debug.Assert(a_filter.Evaluate(src_center_y - (start_yy - 1)).IsAlmostEquals(0));
                    Debug.Assert(a_filter.Evaluate(src_center_y - (end_yy + 1)).IsAlmostEquals(0));

                    a_vert_weights.Add(weights);
                }
            }

            if (a_optimize)
            {
                foreach (var ws in a_vert_weights.Concat(a_horz_weights))
                {
                    double sum = ws.Sum(w => w.Weight);

                    foreach (var w in ws)
                        w.Weight /= sum;

                    for (int i = ws.Count - 1; i >= 0; i--)
                    {
                        if (ws[i].Weight.IsAlmostEquals(0))
                            ws.RemoveAt(i);
                    }

                    ws.Sort();
                }
            }
        }

        public override ResamplerType ResamplerType
        {
            get
            {
                return ResamplerType.Resizer;
            }
        }
    }
}